﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Text
{
    using System;
    using System.Text;

    /// <summary>Provides utility functions for buffers containing encoded character sequences.</summary>
    internal static class EncodedBufferHelper
    {
        /// <summary>Separates the words of the first line in <paramref name="segment"/>, converts them into strings and
        /// assigns them to the elements of <paramref name="words"/>.</summary>
        /// <param name="segment">A segment containing at least one full line, including the "\r\n" sequence.</param>
        /// <param name="encoding">The encoding to transform the bytes in <paramref name="segment"/> into strings.
        /// </param>
        /// <param name="words">An array, the length of which specifies how many words are expected.</param>
        /// <param name="spaceDelimitsWords">The value <c>true</c> if words are delimited by SPACE and TAB; <c>false</c>
        /// if words are only delimited by TAB.</param>
        /// <returns>The index of the first character after the first "\r\n" sequence, or the index immediately past the
        /// last character if no "\r\n" sequence was found.</returns>
        /// <remarks>
        /// <para>If fewer words than <paramref name="words"/>.<see cref="Array.Length"/> are found then the elements
        /// at the end of the array are set to <c>null</c>. If more words are found, then the last element of
        /// <paramref name="words"/> contains the last expected word plus all possibly following unexpected words.
        /// </para>
        /// </remarks>
        internal static int GetFirstLineWords(
            ArraySegment<byte> segment, Encoding encoding, string[] words, bool spaceDelimitsWords)
        {
            var rawWords = new ArraySegment<byte>[words.Length];
            var result = GetFirstLineWords(segment, rawWords, spaceDelimitsWords, true);

            for (var index = 0; index < rawWords.Length; ++index)
            {
                var rawWord = rawWords[index];
                words[index] =
                    rawWord.Array == null ? null : encoding.GetString(rawWord.Array, rawWord.Offset, rawWord.Count);
            }

            return result;
        }

        /// <summary>Separates the words of the first line in <paramref name="segment"/> and assigns them to the
        /// elements of <paramref name="words"/>.</summary>
        /// <param name="segment">A segment containing at least one full line, including the "\r\n" sequence.</param>
        /// <param name="words">An array, the length of which specifies how many words are expected.</param>
        /// <param name="spaceDelimitsWords">The value <c>true</c> if words are delimited by SPACE and TAB; <c>false</c>
        /// if words are only delimited by TAB.</param>
        /// <param name="removeEmptyWords">The value <c>true</c> if zero length words are removed, <c>false</c>
        /// otherwise.</param>
        /// <returns>The index of the first character after the first "\r\n" sequence, or the index immediately past the
        /// last character if no "\r\n" sequence was found.</returns>
        /// <remarks>
        /// <para>If fewer words than <paramref name="words"/>.<see cref="Array.Length"/> are found then the elements
        /// at the end of the array are set to <c>null</c>. If more words are found, then the last element of
        /// <paramref name="words"/> contains the last expected word plus all possibly following unexpected words.
        /// </para>
        /// </remarks>
        internal static int GetFirstLineWords(
            ArraySegment<byte> segment, ArraySegment<byte>[] words, bool spaceDelimitsWords, bool removeEmptyWords)
        {
            unchecked
            {
                Array.Clear(words, 0, words.Length);
                int index = segment.Offset;
                int pastEnd = segment.Offset + segment.Count;
                State state = State.InWhitespace;
                byte currentByte;
                int currentWordStart = index;
                int wordIndex = 0;

                while (index < pastEnd)
                {
                    currentByte = segment.Array[index];

                    switch (state)
                    {
                        case State.InWhitespace:
                            switch ((currentByte == (byte)' ') && spaceDelimitsWords ? (byte)'\t' : currentByte)
                            {
                                case (byte)'\t':
                                    if (!removeEmptyWords)
                                    {
                                        words[wordIndex++] = new ArraySegment<byte>(segment.Array, index, 0);

                                        if (wordIndex == words.Length - 1)
                                        {
                                            state = State.InRemainder;
                                        }
                                    }

                                    break;
                                case (byte)'\r':
                                    state = State.LastWasCarriageReturn;
                                    break;
                                default:
                                    state = (wordIndex < words.Length - 1) ? State.InWord : State.InRemainder;
                                    currentWordStart = index;
                                    break;
                            }

                            break;
                        case State.InWord:
                            switch ((currentByte == (byte)' ') && spaceDelimitsWords ? (byte)'\t' : currentByte)
                            {
                                case (byte)'\t':
                                    state = State.InWhitespace;
                                    words[wordIndex++] = new ArraySegment<byte>(
                                        segment.Array, currentWordStart, index - currentWordStart);
                                    break;
                                case (byte)'\r':
                                    words[wordIndex++] = new ArraySegment<byte>(
                                        segment.Array, currentWordStart, index - currentWordStart);
                                    state = State.LastWasCarriageReturn;
                                    break;
                                default:
                                    break;
                            }

                            break;
                        case State.InRemainder:
                            if (currentByte == (byte)'\r')
                            {
                                words[wordIndex++] = new ArraySegment<byte>(
                                    segment.Array, currentWordStart, index - currentWordStart);
                                state = State.LastWasCarriageReturn;
                            }

                            break;
                        case State.LastWasCarriageReturn:
                            state = State.LastWasLineFeed;
                            break;
                        default: // This should be State.LastWasLineFeed, but doing so decreases code coverage
                            return index;
                    }

                    ++index;
                }

                return index;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private enum State
        {
            InWhitespace,
            InWord,
            InRemainder,
            LastWasCarriageReturn,
            LastWasLineFeed
        }
    }
}
